package com.surrealbananapeel.battleship;

import java.util.ArrayList;
import java.util.LinkedList;
/**
 * This class is used to represent a game played inside a match.
 * The game has a three-dimensional array that stores points states.
 * 
 * @author Emmett Little and Alberto Saavedra
 */
public class Game
{
    private Point.State[][][] board;
    private LinkedList<Point> shots;
    private int realHits;
    private int realDetecteds;
    
    /**
     * Creates an instance of this class by initialising the board
     * with empty shots.
     */
    public Game()
    {
        this.board = new Point.State[BattleshipClient.WIDTH][BattleshipClient.HEIGHT][BattleshipClient.DEPTH];
        this.shots = new LinkedList<Point>();
        initalizeBoard();
    }
    
    /**
     * This method fills out the three-dimensional array with empty point states. 
     */
    private void initalizeBoard()
    {
        for (int i = 0; i < BattleshipClient.WIDTH; i++)
        {
            for (int j = 0; j < BattleshipClient.HEIGHT; j++)
            {
                for (int k = 0; k < BattleshipClient.DEPTH; k++)
                {
                    board[i][j][k] = Point.State.EMPTY;
                }
            }
        }
    }
    
    /**
     * This method updates the board by changing the its points states.
     *  This action records the points fired by our player.
     * 
     * @param point The point to store in the board.
     * @param shipName The shipName that has been identified with the point. 
     */
    public void updateBoard(Point point, String shipName)
    {
        if (point.getState() == Point.State.HIT)
        {
            realHits++;
        }
        shots.add(0, point);
        board[point.getX()][point.getY()][point.getZ()] = point.getState();
        if (point.getState() == Point.State.DETECTED)
        {
            shots.get(0).setState(Point.State.HIT);
            board[point.getX()][point.getY()][point.getZ()] = Point.State.HIT;
            detectShip(shipName);
        }
    }
    
    /**
     * This method is called every time a detected point has been returned from 
     * the server. Then the method detects the specific coordinates of the ship sunk.
     * 
     * @param shipName The ship sunk to detect.
     */
    private void detectShip(String shipName)
    {
        if (Fleet.getShip(shipName) != null)
        {
            int shipSize = Fleet.getShip(shipName).getPoints().length;
            realHits -= shipSize;
            realDetecteds += shipSize;
            boolean searching = true;
            Point p = getlastShot();
            for (int x = -5; x <= 5 && searching; x++)
            {
                for (int y = -5; y <= 5 && searching; y++)
                {
                    for (int z = -5; z <= 5 && searching; z++)
                    {
                        Point dp = new Point(x, y, z);
                        
                        Ship sunk1 = Fleet.getShip(shipName);
                        sunk1.updateOrientation(Ship.Orientation.XY);
                        detectShipCheck(sunk1, p, dp);
                        
                        Ship sunk2 = Fleet.getShip(shipName);
                        sunk2.updateOrientation(Ship.Orientation.XZ);
                        detectShipCheck(sunk2, p, dp);
                        
                        Ship sunk3 = Fleet.getShip(shipName);
                        sunk3.updateOrientation(Ship.Orientation.YZ);
                        detectShipCheck(sunk3, p, dp);
                    }
                }
            }
        }
    }
    
    /**
     * Check the specific ship to see if it is overlapping all hits at the 
     * offset of point p plus point dp and that the ship contains the point p.
     * 
     * @param sunk The ship to check
     * @param p The point of the detected ship
     * @param dp The offset from the detected ship
     */
    private void detectShipCheck(Ship sunk, Point p, Point dp)
    {
        if (sunk.shift(p.getX()+dp.getX(), p.getY()+dp.getY(), p.getZ()+dp.getZ()))
        {
            boolean detected = true;
            boolean contains = false;
            Point[] points = sunk.getPoints();
            for (int i = 0; i < points.length && detected && !contains; i++)
            {
                if (board[points[i].getX()][points[i].getY()][points[i]
                        .getZ()] != Point.State.HIT)
                {
                    detected = false;
                }
                if (points[i].equals(p))
                {
                    contains = true;
                }
            }
            if (detected && contains)
            {
                updateBoardAndShotsToDetected(points);
            }
        }
    }
    
    /**
     * Gets the number of real hits.
     * 
     * @return an Integer with the real hits.
     */
    public int getRealHits()
    {
        return realHits;
    }
    
    /**
     * This method sets the real hits.
     *  
     * @param realHits  The integer to set.
     */
    public void setRealHits(int realHits)
    {
        this.realHits = realHits;
    }
    
    /**
     * Gets the real detected value.
     * 
     * @return an integer with detected points. 
     */
    public int getRealDetecteds()
    {
        return realDetecteds;
    }
    
    /**
     * Sets the real detected point from the board.
     * 
     * @param realDetecteds The integer to set.
     */
    public void setRealDetecteds(int realDetecteds)
    {
        this.realDetecteds = realDetecteds;
    }
    
    /**
     * This method updates the board given a list of detected points.
     * 
     * @param points a list of points.
     */
    private void updateBoardAndShotsToDetected(Point[] points)
    {
        for (int i = 0; i < points.length; i++)
        {
            board[points[i].getX()][points[i].getY()][points[i].getZ()] = Point.State.DETECTED;
            for (int j = 0; j < shots.size(); j++)
            {
                if (shots.get(j).equals(points[i]))
                {
                    shots.get(j).setState(Point.State.DETECTED);
                    break;
                }
            }
        }
    }
    
    /**
     * This method gets a list of hit points.
     * 
     * @return a list of points.
     */
    public ArrayList<Point> getHits()
    {
        return getPointType(Point.State.HIT);
    }
    
    /**
     * This method gets a list of detected points.
     * @return a list of points.
     */
    public ArrayList<Point> getDetecteds()
    {
        return getPointType(Point.State.DETECTED);
    }
    
    /**
     * This method returns a list of points filtered by their state.
     * 
     * @param state The state to filer by.
     * @return A list of points with similar states.
     */
    private ArrayList<Point> getPointType(Point.State state)
    {
        ArrayList<Point> points = new ArrayList<Point>();
        
        for (int i = 0; i < shots.size(); i++)
        {
            if (shots.get(i).getState() == state)
            {
                points.add(shots.get(i));
            }
        }
        return points;
    }
    
    /**
     * This method returns the very last shot made by our player. 
     * 
     * @return The last point played.
     */
    public Point getlastShot()
    {
        if (this.shots.size() == 0)
        {
            return null;
        }
        return shots.get(0);
    }
    
    /**
     * Checks if the shot has been fired yet. It also makes sure the shot is a
     * valid one, if it isn't then false is returned.
     * 
     * @param p
     *            the point to check
     * @return true if it has been shot before or is invalid, true otherwise
     */
    public boolean hasBeenShot(Point p)
    {
        int x = p.getX();
        int y = p.getY();
        int z = p.getZ();
        
        if (x < 0 || x >= 10 || y < 0 || y >= 10 || z < 0 || z >= 10
                || board[x][y][z] != Point.State.EMPTY)
            return true;
        
        return false;
    }
}
